//
//  LastfmTagModule.h
//  LastfmTagModule
//
//  Created by Thomas Backman on 11/21/07.
//  Copyright Thomas Backman 2007. All rights reserved.
//

#import "LastfmTagModuleAction.h"
#import <QSCore/QSNotifyMediator.h>

@implementation LastfmTagModuleAction


- (NSArray *)iTunesDictionariesForTrack:(NSString *)theTrack byArtist:(NSString *)theArtist {
	//
	// Returns an array of dictionaries; each dictionary contains (all) details of an iTunes track, including stuff like bitrate, samplerate, "episode number" etc.
	//

	// Uses an undocumented call to the iTunes module - thanks Alcor :)
	NSDictionary *allTracks = [[[QSReg getClassInstance: @"QSiTunesDatabase"] iTunesMusicLibrary] objectForKey:@"Tracks"];
	if (allTracks == nil)
		return nil;
	
	NSMutableArray *tracks = [[NSMutableArray alloc] init];
	NSEnumerator *trackEnum = [allTracks objectEnumerator];
	
	NSDictionary *track;
	while (track = [trackEnum nextObject]) {
		// Loop through all the tracks in the library, and...
		if ([theTrack compare:[track objectForKey:@"Name"] options:NSCaseInsensitiveSearch] == NSOrderedSame &&
			[theArtist compare:[track objectForKey:@"Artist"] options:NSCaseInsensitiveSearch] == NSOrderedSame) {
			// If both title and artist match, add it to the array to be returned
				[tracks addObject:track];
		}
	}
	
	if ([tracks count] == 0) {
		[tracks release];
		return nil;
	}
	else
		return [tracks autorelease];
}

- (NSArray *)tracksOnAlbum:(NSString *) album byArtist:(NSString *) artist trackNamesOnly:(BOOL) trackNamesOnly {
	//
	// Returns a list of tracknames (with trackNamesOnly set), or a full dictionary with all available info about the tracks on an album.
	//
	NSDictionary *allTracks = [[[QSReg getClassInstance: @"QSiTunesDatabase"] iTunesMusicLibrary] objectForKey:@"Tracks"];
	if (allTracks == nil)
		return nil;
	
	NSMutableArray *tracks = [[NSMutableArray alloc] init];;
	NSEnumerator *trackEnum = [allTracks objectEnumerator];

	NSDictionary *track;
	while (track = [trackEnum nextObject]) {
		// Same deal as above, loop through all tracks and see if Album and Artist match.
		if ([album compare:[track objectForKey:@"Album"] options:NSCaseInsensitiveSearch] == NSOrderedSame &&
			[artist compare:[track objectForKey:@"Artist"] options:NSCaseInsensitiveSearch] == NSOrderedSame) {	   
			if (trackNamesOnly) {
				// If the trackNamesOnly argument was YES, we return an array of track names instead.
				[tracks addObject: [track objectForKey:@"Name"]];
			}
			else {
				[tracks addObject:track];
			}
		}
	}

	if ([tracks count] == 0) {
		[tracks release];
		return nil;
	}
	else
		return [tracks autorelease];
}

- (NSMutableArray *)fetchTracksForTag:(NSString *)tmp {
	//
	// Does what it's named - downloads a list of tracks and returns them (as TBSong objects)
	//
	
	if ([tmp isEqualToString:@"combined objects"]) {
		// Dunno what this is, but something (Quicksilver...?) seems to ask for it now and then.
		return nil;
	}
		
	NSMutableString *tag = [tmp mutableCopy];
	NSMutableArray *songList = [[NSMutableArray alloc] init];
	
	// Prepare tag (since it'll be in the URL)
	[tag replaceOccurrencesOfString:@" " withString:@"+" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [tag length])];
	
	NSString *username = [[NSUserDefaults standardUserDefaults] stringForKey:@"Last.fm username"];
	if (username == nil || [username isEqualToString:@"YourUsername"]) {
		// User needs to set things up
		[[NSAlert alertWithMessageText:@"You need to setup the Last.fm tag module before using it!" defaultButton:@"Enough already, I will!" alternateButton:nil otherButton:nil informativeTextWithFormat:@"You haven't changed your Last.fm username! Open the Quicksilver preferences and do something about it!"]
						  runModal];
		[songList release];
		[tag release];
		return nil;
	}
	
	NSMutableString *urlString = [NSMutableString stringWithFormat:@"http://ws.audioscrobbler.com/1.0/user/%@/taggeditems.xml?tag=%@", username, tag];
	NSError *err = nil;
	
	// Fetch the tagged tracks
	NSXMLDocument *xmlDocument = [[NSXMLDocument alloc] initWithContentsOfURL:[NSURL URLWithString:urlString] options:NSXMLDocumentTidyXML error:&err];
	if (err != nil) {
		NSString *errtext = [err localizedDescription];

		[[NSAlert alertWithMessageText:@"Unable to fetch tagged tracks - are you connected to the internet?" defaultButton:@"OK" alternateButton:nil otherButton:nil 
			informativeTextWithFormat:@"Raw error message:\n%@", errtext] runModal];
		[tag release];
		return nil;
	}
	
	// Extract tracks with XPath, and add the found songs to a list
	for (NSXMLElement *xmlElement in [xmlDocument nodesForXPath:@"./taggeditems/track" error:&err])
	{
		NSXMLElement *artist = (NSXMLElement *)[xmlElement childAtIndex:0];
		if ([[artist name] isEqualToString:@"artist"]) // Make sure that this is an <artist> tag, despite the variable name ;) All we know is that it's the first child.
		{
			TBSong *song = [TBSong songWithTitle:[[xmlElement attributeForName:@"name"] stringValue] andArtist:[[artist attributeForName:@"name"] stringValue]];
			
			// Make sure we don't add a song already in there
			if (![songList containsObject:song])
				[songList addObject:song];
			
		}
	}
	
	if (![[NSUserDefaults standardUserDefaults] integerForKey:@"Last.fm include tagged albums"]) {
		// Should we include all tracks on <album>s with the specified tag, too? If not, lets quit.
		goto end; // to avoid a set of curly braces
	}

	// Same deal, but for albums!		
	for (NSXMLElement *xmlElement in [xmlDocument nodesForXPath:@"./taggeditems/album" error:&err])
	{
		NSXMLElement *artist = (NSXMLElement *)[xmlElement childAtIndex:0];
		if ([[artist name] isEqualToString:@"artist"]) // Make sure that this is an <artist> tag, despite the variable name ;) All we know is that it's the first child.
		{
			NSString *albumName = [[xmlElement attributeForName:@"name"] stringValue];
			NSString *artistName = [[artist attributeForName:@"name"] stringValue];
			NSArray *albumTracks = [self tracksOnAlbum:albumName byArtist:artistName trackNamesOnly:YES];
			if (albumTracks == nil)
				continue;
			
			for (NSString *songTitle in albumTracks)
			{
				TBSong *song = [TBSong songWithTitle:songTitle andArtist:artistName];
				
/*				if (![songList containsObject:song]) {
					[songList addObject:song];
				}*/
				for (TBSong *s in songList) {
					if ([s isEqualToSong:song])
						goto next_song; 
				}
				[songList addObject:song];
			next_song: if (0) {};
			}
		}
		
	}
end:
	[xmlDocument release];
	[tag release];
	return [songList autorelease];
}

- (QSObject *)createPlaylistFromObjectUsingDefaultMode:(QSObject *)dObject {
	// Default function, uses user setting
	int mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"Last.fm multiple tag mode"];
	return [self createPlaylistFromObject:dObject usingMode:mode];
}

- (QSObject *)createPlaylistUsingAND:(QSObject *)dObject {
	// Force AND mode
	return [self createPlaylistFromObject:dObject usingMode:AND];
}

- (QSObject *)createPlaylistUsingOR:(QSObject *)dObject {
	// Force OR mode
	return [self createPlaylistFromObject:dObject usingMode:OR];
}

// The real deal:
- (QSObject *)createPlaylistFromObject:(QSObject *)dObject usingMode:(NSInteger)mode
{
	//
	// Takes one or more tags as arguments, creates a playlist, and (depending on settings) starts playing it
	//
	
	NSMutableArray *tags = [NSMutableArray array];
	NSString *playlistName = [[NSUserDefaults standardUserDefaults] stringForKey:@"Last.fm playlist name"];

	int tagcount = [dObject count];
	if (tagcount == 1) {
		// If only a single tag was given (no comma trick), we're cool.
		[tags addObject:[dObject stringValue]];
		if ([playlistName isEqualToString:@""]) {
			playlistName = [NSString stringWithFormat:@"Tag: %@", [dObject stringValue]];
		}
	}
	else {
		// Multiple tags given
		if (mode == AND && [dObject count] != 2) // AND mode and more than two objects
		{
			NSRunAlertPanel(@"AND only works with two tags", @"You cannot specify more than two tags when using AND. Please either use two tags, or change your settings to OR under Quicksilver Preferences -> Last.fm tag module.", nil, nil, nil);
			return nil;
		}
		for (QSObject *q in [dObject children]) {
			// Add 'em all
			[tags addObject:[q stringValue]];
		}
		
		// Name the playlist, if needed
		if ([playlistName isEqualToString:@""]) {
			if (mode == AND) {
				playlistName = [NSString stringWithFormat:@"Tag: %@ & %@", [tags objectAtIndex:0], [tags objectAtIndex:1]];
			}
			else if (mode == OR) {
				NSMutableString *tmpName = [NSMutableString stringWithString:@"Tag: "];
				
				for (NSString *tag in tags) {
					[tmpName appendFormat:@"%@ | ", tag];
				}
				
				// Remove the last "|"
				[tmpName deleteCharactersInRange:NSMakeRange([tmpName length]-3, 3)];

				playlistName = [NSString stringWithString:tmpName];
			}
		}		
	}

	// Create playlist if it doesn't already exist
	NSAppleScript * script = [NSAppleScript scriptWithContentsOfResource:@"iTunesmanager.scpt" inBundle:[NSBundle bundleForClass:[self class]]];
	[script executeSubroutine:@"create_playlist" arguments:playlistName error:nil];
		
	// Clear out playlist if needed
	if ([[NSUserDefaults standardUserDefaults] integerForKey:@"Last.fm playlist mode"] == REPLACE)
	{
		/* NSAppleScript * */ script = [NSAppleScript scriptWithContentsOfResource:@"iTunesmanager.scpt" inBundle:[NSBundle bundleForClass:[self class]]];
		[script executeSubroutine:@"clear_playlist" arguments:playlistName error:nil];
	}

	if (tagcount == 1) {
		songs = [self fetchTracksForTag:[tags objectAtIndex:0]];
	}
	else
	{
		//
		// Multiple tags, we need to do different things depending on the mode (0 == OR, 1 == AND)
		// Yes, this code is UGLY. Yes, it is hard to understand. But it works, and I'm not touching this at the moment...
		// Just reading it makes me feel tired.
		//
		
		int loop = -1; // to make things easier (not easier to understand, mind you)
		for (NSMutableString *tmp in tags)
		{
			loop++;
			if (mode == OR)
			{
				if (tmpsongs != nil) // not the first loop
				{
					if (songs == nil)
					{
						songs = tmpsongs;
						tmpsongs = [self fetchTracksForTag:tmp];
					}
					else
					{
						//third, or fourth, or... loop, merge the lists so far, and free up tmpsongs for the next loop, if there is one
					
						// Add the found tracks unless they're already in there
						for (TBSong *newSong in tmpsongs) {
							if (![songs containsObject:newSong]) { [songs addObject:newSong]; }
						}
						if (tmpsongs) [tmpsongs release]; // FIXME: should we really release this (autoreleased?)?
						tmpsongs = [self fetchTracksForTag:tmp]; // ... and fetch the tracks for the most recent tag for the next loop
						continue;
					}
				}
				else
					tmpsongs = [self fetchTracksForTag:tmp];
			}
			else if (mode == AND)
			{
				if (loop == 0)
					songs = [self fetchTracksForTag:tmp];
				else if (loop == 1)
					tmpsongs = [self fetchTracksForTag:tmp];
				else 
					return nil; // should never happen, tagcount is checked first thing in this method
			}
			
			else { [[NSAlert alertWithMessageText:@"Playlist mode needs to be either 0 (OR) or 1 (AND). Did you hack the plist? :)"
			 defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:nil] runModal]; return nil; }

		}
		
		if (tmpsongs != nil && songs != nil)
		{
			// Merge the tracks, for the last time
			if (mode == OR)
			{
				for (TBSong *newSong in tmpsongs) {
					if (![songs containsObject:newSong]) { [songs addObject:newSong]; } // Just add, but watch for duplicates
				}
			}
			else if (mode == AND)
			{
				NSMutableArray *tag1 = tmpsongs;
				NSMutableArray *tag2 = songs;
				tmpsongs = songs = nil;
				songs = [[NSMutableArray alloc] init];
				
				for (TBSong *newSong in tag1) {
					if ([tag2 containsObject:newSong]) { [songs addObject:newSong]; } // Only add if the song exists both in tag1 and tag2
				}
			}
		}
	}
	
	if ([songs count] < 1)
	{
		if ([[NSUserDefaults standardUserDefaults] boolForKey:@"Last.fm show notification"])
		{
			id notifier = [QSReg preferredNotifier];
			NSDictionary *notificationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
			@"No tracks matched your tag(s)!", QSNotifierTitle,
			@"It's also possible that some error occured on the way from the net to your computer.", QSNotifierText,
			nil];
			
			[notifier displayNotificationWithAttributes:notificationDictionary];
		}
		return nil;
	}
		
	// OK, to make things "easy", we serialize the array and pass it to the script that way.
	// The format is: "Playlist name<three nulls>Artist #1<null>Song #1<two nulls>Artist #2<null>...etc"
	
	NSMutableString *str = [NSMutableString stringWithFormat:@"%@\0\0\0", playlistName];
	for (TBSong *song in songs)
		[str appendFormat:@"%@\0%@\0\0", [song artist], [song songTitle]];
	
	tmpsongs = songs = nil;

	script = [NSAppleScript scriptWithContentsOfResource:@"iTunesmanager.scpt" inBundle:[NSBundle bundleForClass:[self class]]];
	[script executeSubroutine:@"add_tracks" arguments:str error:nil];
	
	// Show notification via Growl (or Quicksilver?)
	if ([[NSUserDefaults standardUserDefaults] boolForKey:@"Last.fm show notification"]) {
		id notifier = [QSReg preferredNotifier];
		NSDictionary *notificationDictionary = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"Playlist generation complete", @"", nil] 
		forKeys:[NSArray arrayWithObjects:QSNotifierTitle, QSNotifierText, nil]];
 
		[notifier displayNotificationWithAttributes:notificationDictionary];
	}
	
	if ([[NSUserDefaults standardUserDefaults] boolForKey:@"Last.fm start playing automatically"])
	{
		NSAppleScript *script = [[NSAppleScript alloc] initWithSource:[NSString stringWithFormat:@"tell application \"iTunes\"\n\
		tell user playlist \"%@\"\n\
			play\n\
		end tell\n\
												 end tell\n", playlistName]];
		[script executeAndReturnError:nil];
		[script release];
	}
	
	return nil;
}

@end